/**
 * @module ChiVoxSDK/html5
 * @author Don Li <kai.li@chivox.com>
 * @licence
 * Copyright (c) 2017 - 2020 ChiVox Inc. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import Utils from '../utils';
import cfg from '../config.json';
import {
    get_sig
} from '../lib/public';

import VolumeBar from './volumebar';
import Html5Player from './html5player';
import RecordWorker from 'worker-loader?inline&fallback=false!../worker/recordWorker';
//import SpeexWorker from 'worker-loader?inline&fallback=false!../../worker/speexWorker';

/**
 * HTML5模式的录音机。<br/><br/>
 *
 * 基于navigator.getUserMedia技术设计开发，用于支持HTML5模式的录音, 同时进行在线评测。
 */
export class Html5Recorder {
    /**
     * 创建一个Html5录音机。
     * @param {Object} options - 初始化参数。格式参见：{@link Default_ChiVoxRecorder_Options}
     */
    constructor(options) {
        this.options = options;

        this.recording = false;
        this.__curr_record_param = null;
        this.__stop_timer = null;

        this.__cnt = 0;
        this.__websocket = null;
        this.__buf_ws = null;
        this.__buf_rec = [];

        this.__audio_replay = null;
        this.__on_replay_stop = null;

        this.canRecord = false;

        navigator.getUserMedia({
            audio: true
        }, (stream) => {
            this.gotStream(stream)
        }, function(e) {
            console.error("no audio deivce");
            options.onError("no audio device!");
        });

        this.get_sig = get_sig;
        this.test_connection();
    }

    gotStream(stream) {
        let AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
        this.context = new AudioContext();

        let bufferLen = Utils.buff_size();
        let outputArray = new Int8Array(258);
        let realAudioInput = this.context.createMediaStreamSource(stream);

        this.inputPoint = this.context.createGain();
        this.audioInput = realAudioInput;
        this.audioInput.connect(this.inputPoint);

        this.analyserNode = this.context.createAnalyser();
        this.analyserNode.fftSize = 2048;
        this.inputPoint.connect(this.analyserNode);

        this.__volumebar = new VolumeBar(`#${this.options.recorderid}`, this.context, this.analyserNode, realAudioInput);

        this.node = this.context.createScriptProcessor(bufferLen, 1, 1);

        this.record_worker = new RecordWorker();
        this.record_worker.onmessage = (e) => {
            if (e.data.command == "debug") {} else if (e.data.command == "blob") {
                if (!this.__audio_replay)
                    this.__audio_replay = document.createElement('audio');

                try {
                    // Fallback if createObjectURL is not supported
                    var fileReader = new FileReader();
                    fileReader.onload = (event) => {
                        this.__audio_replay.src = event.target.result;
                    };

                    fileReader.readAsDataURL(e.data.blob);
                } catch (err) {
                    console.log(err);
                }

                this.__audio_replay.type = "audio/wav";
                this.__audio_replay.loop = false;
                this.__audio_replay.addEventListener("ended", () => {
                    if (this.__on_replay_stop)
                        this.__on_replay_stop();
                }, false);
            } else {
                if (this.options.onVolume)
                    this.options.onVolume(e.data.volume);

                let buffer = e.data.buffer;
                let result = new Int16Array(buffer.length);
                for (var i = 0; i < buffer.length; i++)
                    result[i] = buffer[i];

                this.send_audio(0, result);
            }
        };

        this.record_worker.postMessage({
            command: 'init',
            config: {
                sampleRate: this.context.sampleRate,
                outputBufferLength: bufferLen
            }
        });

        this.node.onaudioprocess = (e) => {
            if (!this.recording) return;

            this.record_worker.postMessage({
                command: 'record',
                buffer: e.inputBuffer.getChannelData(0)
            });
        };

        this.inputPoint.connect(this.node);
        this.node.connect(this.context.destination);
        this.canRecord = true;
    }

    /*
     * type: 0 => connect, 1 => start, 2 => stop
     */

    __json_param(type, params) {
        var json = null;

        if (type === 0) {
            json = {
                sdk: {
                    version: 0x01000000,
                    source: 0x04,
                    protocol: 'websocket'
                },
                app: {
                    applicationId: this.options.appKey,
                    sig: params.sig,
                    timestamp: params.timestamp,
                    userId: params.userId,
                    alg: this.options.alg
                }
            };
        } else if (type === 1) {
            var uuid = Utils.uuid();

            json = {
                tokenId: uuid,
                audio: {
                    audioType: 'wav',
                    channel: 1,
                    sampleRate: 16000,
                    sampleBytes: 2
                },
                request: params
            };

            this.__curr_record_param.onRecordIdGenerated(0, {
                recordId: uuid
            });
            console.debug('[ start param ]:', json);
        }

        return JSON.stringify(json);
    }


    __startRecord(params) {
        this.recording = true;
        this.isEnd = false;
        this.__buf_ws = [];

        var prev_param = this.__curr_record_param;
        this.__curr_record_param = params;

        this.__timer_stop = setTimeout(() => {
            this.stopRecord();
        }, parseInt(params.duration));

        params.serverParams.sig = params.sig;
        params.serverParams.timestamp = params.timestamp;

        if (!prev_param || this.__websocket == null)
            this.__connect(params.serverParams);
        else {
            if (prev_param.coreType !== params.coreType)
                this.__change_coretype(params.serverParams);
            else if (this.__websocket.readyState != WebSocket.OPEN) {
                this.__connect(params.serverParams);
            }
        }

        params.onStart();

        let _cmd_start = this.__json_param(1, params.serverParams);
        if (this.__websocket.readyState == WebSocket.OPEN) {
            this.__websocket.send(_cmd_start);
        } else {
            this.__buf_ws.push(_cmd_start);
        }

        this.__buf_rec = [];
        if (params.playDing) {
            let player = new Html5Player({});

            if (player.can_play) {
                player.load({
                    url: `${cfg.host}/static/ding.mp3`,
                    success: (code, message) => {
                        player.play({
                            position: 0,
                            onStop: (code, message) => {
                                this.record_worker.postMessage({
                                    command: 'reset'
                                });
                            }
                        });
                    }
                });
            }
        } else {
            this.record_worker.postMessage({
                command: 'reset'
            });
        }
    }

    /** 停止录音 */
    stopRecord() {
        if (!this.recording) return;
        this.recording = false;

        if (this.__timer_stop) {
            clearTimeout(this.__timer_stop);
            this.__timer_stop = null;
        }

        this.send_audio(1, null);
        this.__timer_wait_res = setTimeout(this.__on_coreTimeout, cfg.core_timeout);

        this.record_worker.postMessage({
            command: 'exportAudio',
            type: "wav"
        });
        this.__curr_record_param.onStop(0, '');
    }

    /**
     * 开始回放最后一次录音。
     *
     * @param {Object} options - 回放时所需的参数。参数有：
     * @param {string} options.recordId - 要回放的录音recordId。
     * @param {callback} options.onStop - 回放完成的Callback，格式：() => {}。
     */
    startReplay(option) {
        if (!this.__audio_replay) {
            throw new Error('no record audio');
        } else {
            this.__on_replay_stop = option.onStop;
            this.__audio_replay.play();
        }
    }

    /** 停止回放。*/
    stopReplay() {
        if (this.__audio_replay) {
            this.__audio_replay.pause();
            this.__audio_replay.currentTime = 0;

            if (this.__on_replay_stop)
                this.__on_replay_stop();
        }
    }

    /**
     * 开始录音。
     *
     * @param {Object} params - 录音时所需的参数。参数有：
     * @param {string} params.duration - 录音时长，单位：毫秒。
     * @param {boolean} params.playDing - 录音前是否播放“ding”。
     * @param {Object} params.serverParams - 录音参数。详见；{@link Default_ChiVoxRecorder_Options}
     * @param {Callback} params.onRecordIdGenerated - 开始录音后生成recordId后的Callback。格式：(code, message) => { var lastRecordID = message.recordId; }
     * @param {Callback} params.onStart - 开始录音后的Callback。格式：() => { }
     * @param {Callback} params.onStop - 录音结束后的Callback。格式：(code, message) => { }
     * @param {Callback} params.onInternalScore - 录音中服务器Push回来的中间评分结果的Callback。格式：(code, message) => { }
     * @param {Callback} params.onScore - 服务器评分结果返回后的Callback。格式：(data) => { }
     * @param {Callback} params.onScoreError - 服务器评分出错返回后的Callback。格式：(code, message) => { }
     */
    record(params) {
        if (this.recording) {
            options.onError('please wait the last record end!');
            return;
        }

        if (!this.record_worker) {
            options.onError('no input device!');
            return;
        }

        if (typeof this.options.sig != 'undefined') {
            params.sig = this.options.sig;
            params.timestamp = this.options.timestamp;
        }

        var current_t = Math.floor(new Date().getTime() / 1000);

        if (params.sig !== '' && params.timestamp !== undefined && parseInt(params.timestamp) >= (current_t - cfg.timeout)) {
            this.__startRecord(params);
        } else {
            this.get_sig(params);
        }
    }

    /** 重置录音机。*/
    reset() {
        this.stopReplay();
        this.stopRecord();
    }

    /** 显示波形图。*/
    showVolumeBar() {
        this.__volumebar.show();
    }

    test_connection() {
        var onConnectorStatusChange = this.options.onConnectorStatusChange;
        onConnectorStatusChange(50109, 0);

        var webSocket;
        try {
            websocket = new WebSocket(`${this.options.server}/en.sent.score`);
        } catch (err) {
            onConnectorStatusChange(50101, 0);
            return;
        }

        websocket.onopen = (e) => {
            onConnectorStatusChange(50100, 0);
            websocket.close;
        };
        websocket.onerror = (e) => {
            onConnectorStatusChange(50101, 0);
        };
    }

    send_audio(state, data) {
        this.__cnt++; //send data when __cnt == 6

        if (1 != state)
            this.__buf_rec.push(data);

        if (1 == state || 6 >= this.__cnt) {
            var outputArray;

            if (this.__buf_rec.length > 0) {
                var output = this.__buf_rec.splice(0, this.__buf_rec.length);
                var i;
                outputArray = new Int16Array(output.length * 320);
                for (i = 0; i < output.length; i++)
                    outputArray.set(output[i], i * 70);

                if (this.__websocket.readyState == WebSocket.OPEN) {
                    this.__websocket.send(outputArray.buffer);
                } else {
                    this.__buf_ws.push(outputArray.buffer);
                }
            }

            this.__cnt = 0;
            if (state == 1) {
                if (this.__websocket.readyState == WebSocket.OPEN) {
                    this.__websocket.send(new ArrayBuffer(0));
                }
            }
        }
    }

    __connect(params) {
        var url = `${this.options.server}/${params.coreType}`;
        if (params.res)
            url += `/${params.res}`;

        url += '?e=0&t=0&version=2';

        this.__log(1, 19, url, '');

        this.__websocket = new WebSocket(url);
        this.__websocket.onopen = (e) => {
            this.canRecord = true;

            this.__websocket.send(this.__json_param(0, params));

            for (var i = 0; i < this.__buf_ws.length; i++)
                this.__websocket.send(this.__buf_ws[i]);
            this.__buf_ws = [];

            this.__log(1, 15, url, '');
        };

        this.__websocket.onclose = () => {
            this.canRecord = false;
            this.__log(1, 17, url, '');
        };
        this.__websocket.onmessage = (e) => {
            if (this.__timer_wait_res)
                clearTimeout(this.__timer_wait_res);

            let json = JSON.parse(e.data);

            console.debug('[ onmessage ]: ', json);

            if (json.error) {
                this.stopRecord(true);
                this.options.onError(`error from server. detail: ${json.error}`);

                if (this.__curr_record_param && this.__curr_record_param.onScoreError) {
                    this.__curr_record_param.onScoreError(50201, json.error);
                } else {
                    console.error('no onScoreError callback.', json);
                }
            } else {
                if (json.result.snt) {
                    if (this.__curr_record_param && this.__curr_record_param.onInternalScore) {
                        this.__curr_record_param.onInternalScore(0, json);
                    } else {
                        console.error('no onInternalScore callback.', json);
                    }
                } else {
                    //this.__dict_res[json.tokenId] = json;
                    if (this.__curr_record_param && this.__curr_record_param.onScore) {
                        this.__curr_record_param.onScore(json);
                    } else {
                        console.error('no onScore callback.', json);
                    }
                }
            }
        };

        this.__websocket.onerror = (e) => {
            this.stopRecord(true);
            this.options.onError('websocket error.');
            this.canRecord = false;

            this.__log(1, 16, url, e.data);
        };
    }

    __disconnect() {
        this.__websocket.close();
        this.__websocket = null;
        this.canRecord = false;
    }

    __change_coretype(params) {
        this.__disconnect();
        this.__connect(params);
    }

    __on_coreTimeout() {
        this.__curr_record_param.onStop(50100, 'core time out');

        if (this.__curr_record_param && this.__curr_record_param.onScoreError) {
            this.__curr_record_param.onScoreError.call(50100);
        } else {
            console.error('core timeout, and no onScoreError callback.');
        }
    }

    /** 发送websocket连接日志 */
    __log(evtid, statusid, serverurl, reason) {
        let uid = (this.options.data && this.options.data.serverParams && this.options.data.serverParams.userId) ? this.options.data.serverParams.userId : 'unknown';
        let url = `${cfg.logserver}?eid=${evtid}&est=${statusid}&applicationId=${this.options.appKey}&uid=${uid}`;
        let params = {
            conn_url: encodeURIComponent(serverurl),
            reason: reason,
            user_agent: navigator.userAgent,
            prot: 1
        };

        let xreq = new XMLHttpRequest();

        xreq.open('POST', url, true);
        xreq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xreq.send('log=' + JSON.stringify(params));
    }
}

export default Html5Recorder
